var path = require('path');

var TinyBlock = require('./TinyBlock');
var AppUtils = require('websocket_p2pnet').AppUtils;

/**
 * 区块链账本
 */
class NoteBook {
  /**
   * 区块链账本
   * @param mineDiffcultLevle 
   */
  constructor() {
    this.appUtils = AppUtils.getSingle();

    // 1.设置配置文件路径，获取工具单例
    var webConfigPath = path.join(__dirname, './WebConfig.json');
    this.appUtils = AppUtils.getSingle(webConfigPath);//获取AppUtils单例

    // 2.获取配置文件中的 挖矿难度系数（数值 n）
    var mineDiffcultLevel = this.appUtils.getWebAppConfigObj().mineDiffcultLevel;
    // 挖矿难度字符串 n-> n个0
    this.mineDiffcultStr = this.setMineDiffcultStr(mineDiffcultLevel);
  
    // 3.区块数据文件路径
    this.dataFilePath = path.join(__dirname, '/NoteBookData.json');
    // 4.创世区块的 preHash：64个'0'
    this.firstBlockHash = '0000000000000000000000000000000000000000000000000000000000000000';
    // 5.区块数组
    this.blockList = [];
    // 6.从硬盘中加载区块数组数据（没有blockList就是空数组）
    this.loadList4Disk();
  }

  /**
   * 0.0 获取账本单例对象
   */
  static getSingle() { 
    if (!this.onlyNoteBook) {
      this.onlyNoteBook = new NoteBook();
    }
    return this.onlyNoteBook;
  }

  /**
   * 0.0 设置挖矿难度 (num -> num 个 0)
   * @param diffcultLevle 难度水平
   */
  setMineDiffcultStr(diffcultLevle) {
    diffcultLevle = parseInt(diffcultLevle); // 3
    var diffcultStr = '';
    for (var i = 0; i < diffcultLevle; i++) {
      diffcultStr += '0';
    }
    return diffcultStr;
  }

  /**
   * 0.1 读取本地磁盘上的区块账本数据
   * @returns Json数组对象
   */
  loadList4Disk() {
      var objList = this.appUtils.fileOpe.loadJsonObjFromDisk(this.dataFilePath);
      this.blockList = this.jsonList2NoteList(objList);
  }

  /**
   * 0.2 将区块账本数据存入硬盘
   */
  savaList2Disk() { 
    var jsonStr = JSON.stringify(this.blockList);
    this.appUtils.fileOpe.save2Disk(this.dataFilePath, jsonStr);
  }

  /**
   * 0.3 将账本json数组的 转成 区块数组
   * @param jsonList Json数组对象
   */
  jsonList2NoteList(jsonList) {
    var blockList = [];
    if (jsonList instanceof Array && jsonList.length > 0) { 
      jsonList.forEach(ele => {
        blockList.push(TinyBlock.json2TinyBlock(ele));
      });
    }
    return blockList;
  }

  // ---------------------------------
  /**
   * 1.0 添加区块
   * @param {string} content 区块内容
   * @param {string} preHash 上一个区块的 hash
   */
  addNoteToBook(content,preHash) { 
    //计算符合要求的 nounce
    var nounce = this.mine(content, preHash);
    var block = new TinyBlock(this.blockList.length + 1, content, new Date().getTime(), null, nounce, preHash);
    block.hash = this.appUtils.HashOpe.sha256(nounce + block.content + block.preHash);

    this.blockList.push(block);
    this.savaList2Disk();
    return block;
  }

  /**
   * 1.1 添加创世区块
   * @param genesis 内容
   */
  addGenesis(genesis) { 
    if (this.blockList.length > 0) {
      throw new Error("添加创世区块的时候,必须保证日记本是空的 - " + this.blockList.length);
    } else if (!genesis || !genesis.length || genesis.length <= 0) {
      throw new Error("区块内容不能为空！");
    }
    // 添加新区块
    var block = this.addNoteToBook(genesis, this.firstBlockHash);
    return block;
  }

  /**
   * 1.2 添加普通区块
   * @param content 区块内容
   */
  addNote(content) { 
    if (this.blockList.length < 1) {
      throw new Error("添加普通区块之前必须有创世区块~！");
    }
    if (content.length == 0) { 
      throw new Error("区块内容不能为空~！");
    }
    // 取出链中最后一个区块（用来获取 上一个区块的 hash）
    var preBlock = this.blockList[this.blockList.length - 1];
    // 添加新区块
    var block = this.addNoteToBook(content,preBlock.hash);
    return block;
  }

  /**
   * 1.3 直接添加区块
   * @param noteBlock 区块对象
   */
  addNoteBlock(newNoteBlock) {
    if (this.blockList.length < 1) {
      throw new Error("添加普通区块之前必须有创世区块~！");
    }

    // 如果传入的不是 区块对象，则认为是 Json格式的区块字符串
    if (!newNoteBlock instanceof TinyBlock && typeof newNoteBlock == 'string') { 
      newNoteBlock = TinyBlock.json2TinyBlock(newNoteBlock);
    }

    //1.获取上一个区块
    var preBlock = this.blockList[this.blockList.length - 1];
    //2.计算 新区块 的nounce是否与上一个区块一致
    var newCulHash = this.appUtils.HashOpe.sha256(newNoteBlock.nounce + newNoteBlock.content + preBlock.hash);
    //3.对比
    if (newCulHash == newNoteBlock.hash) {
      this.blockList.push(newNoteBlock);
      this.savaList2Disk();
    } else {
      console.log('@@@@@@区块数据伪造，不能添加！@@@@@@@');
      return false;
    }
    return true;
  }

  /**
   * 2.1 挖矿( 工作量证明 + 区块内容 + 上个区块hash )
   * @param content 区块内容
   * @param preHash 上个区块hash
   * @returns 工作量证明( nounce )
   */
  mine(content, preHash) {
    var msgStr = '';

    // 工作量证明
    var nounce = -1;
    // 循环 计算 符合规则的 hash值
    for (; nounce < Number.MAX_SAFE_INTEGER; nounce++) {
      //1.获取 挖矿 hash 值
      var mineStr = this.appUtils.HashOpe.sha256(nounce + content);
      if (mineStr.startsWith(this.mineDiffcultStr)) { // 000
        msgStr += '\n【挖矿成功：' + nounce + ',hash=' + mineStr + '】\n';
        break;
      }
      msgStr += '挖矿失败' + nounce + ' ';
    }

    //打印 挖矿过程
    console.log(msgStr);

    if (nounce < -1) { 
      throw new Error('挖矿失败~！' + nounce);
    }

    return nounce;
  }


  /**
   * 2.2 校验数据 
   * @param blockList 要检验的区块数据，如果存在则检查，如果不存在，则检查当前账本的区块数据
   * @returns 校验结果CheckResult数组
   */
  check(blockList) {
    //如果传入了参数，则判断是否为有内容数组，如果不满足条件，则是检查当前账本
    if (!blockList || !(blockList instanceof Array) || blockList.length < 1) {
      blockList = this.blockList;
    }

    var checkResults = []; // 所有区块检查的结果(element -> CheckResult)

    var curBlock = null; // 当前循环的 区块
    var preBlock = null; // 当前循环的 上一个区块
    var culHash = null; // 根据当前区块 内容 + 上一个区块hash 计算当前区块 的hash
    var preHash = null; // 上一个区块 的 hash

    //1.循环 当前账本 的 区块数组，检查 区块的 nounce+content+prehash 工作值 与 上一个区块 的 hash值 是否匹配
    for (var i = 0; i < blockList.length; i++) { // 注意：从下标为1开始取，确保 preBlock 是 i - 1
      //2. 获取前后两个区块
      curBlock = blockList[i];
      preBlock = (i == 0) ? null : blockList[i - 1];

      //3.计算 curBlock 的 hash，如果是curBlock 是第一个区块，则上一个区块直接从变量中取
      preHash = (i == 0) ? this.firstBlockHash : preBlock.hash;
      console.log('preHash=' + preHash);
      culHash = this.appUtils.HashOpe.sha256(curBlock.nounce + curBlock.content + preHash);

      //3.1 对比 区块中保存的 hash 是否与 计算出来的一致
      if (curBlock.hash != culHash) { 
        checkResults.push('编号为【' + curBlock.id + '】的区块内容被篡改了!');
      }

      //3.2 对比 当前区块里保存的 preHash 和 上一个区块的 hash是否一致
      if (i > 0 && preBlock.hash != curBlock.preHash) {
        checkResults.push('编号为【' + curBlock.id + '】的preHash被篡改了!');
      }
    }

    return checkResults;
  }

  /**
   * 2.3 更新账本数据
   * @param noteBlockList 新的账本区块数组
   */
  updateEntireBook(noteBlockList) {
    //转成 区块数组
    noteBlockList = this.jsonList2NoteList(noteBlockList);
    //对比 新数据 和 当前账本数据 的长度
    if (noteBlockList.length <= this.blockList.length) { 
      this.appUtils.logger.logSContent('updateEntireBook', '更新区块账本失败，账本长度一样！');
      return false;
    }

    //校验传入的区块数组
    var checkResultList = this.check(noteBlockList);
    if (checkResultList.length == 0) {
      this.blockList = noteBlockList;
      this.savaList2Disk();
      this.appUtils.logger.logSContent('updateEntireBook', '更新区块账本成功，并存入硬盘~！');

      return true;
    } else { 
      this.appUtils.logger.logSContent('updateEntireBook', '更新区块账本失败~！');
    }
    return false;
  }

  /**
   * 2.4 清空并删除区块数据
   */
  clearDataAndFile() { 
    this.blockList = [];
    this.appUtils.fileOpe.deleteFile(this.dataFilePath);
  }
  // ----------------------------------------------------------------------

  /**
   * 3.1 将区块JSON数组 转成 区块数组
   * @returns 区块数组
   */
  jsonList2NoteList(jsonList) {
    //如果是Json数组字符串则自动转成json对象数组
    jsonList = this.appUtils.getObj4JSON(jsonList);
    var blockList = [];
    if (jsonList && jsonList instanceof Array) {
      jsonList.forEach(ele => {
        blockList.push(TinyBlock.json2TinyBlock(ele));
      });
    }
    return blockList;
  }

  /**
   * 3.2 获取区块数组的 json字符串
   */
  getListJson() { 
    return JSON.stringify(this.blockList);
  }

}

//区块账本单例
AppUtils.onlyNoteBook = null;

module.exports = NoteBook;